{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Project 1 - Implementing Confirmation Codes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our code so far:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import itertools\n",
    "import numbers\n",
    "from datetime import timedelta\n",
    "\n",
    "class TimeZone:\n",
    "    def __init__(self, name, offset_hours, offset_minutes):\n",
    "        if name is None or len(str(name).strip()) == 0:\n",
    "            raise ValueError('Timezone name cannot be empty.')\n",
    "            \n",
    "        self._name = str(name).strip()\n",
    "        # technically we should check that offset is a\n",
    "        if not isinstance(offset_hours, numbers.Integral):\n",
    "            raise ValueError('Hour offset must be an integer.')\n",
    "        \n",
    "        if not isinstance(offset_minutes, numbers.Integral):\n",
    "            raise ValueError('Minutes offset must be an integer.')\n",
    "            \n",
    "        if offset_minutes < -59 or offset_minutes > 59:\n",
    "            raise ValueError('Minutes offset must between -59 and 59 (inclusive).')\n",
    "            \n",
    "        # for time delta sign of minutes will be set to sign of hours\n",
    "        offset = timedelta(hours=offset_hours, minutes=offset_minutes)\n",
    "\n",
    "        # offsets are technically bounded between -12:00 and 14:00\n",
    "        # see: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets\n",
    "        if offset < timedelta(hours=-12, minutes=0) or offset > timedelta(hours=14, minutes=0):\n",
    "            raise ValueError('Offset must be between -12:00 and +14:00.')\n",
    "            \n",
    "        self._offset_hours = offset_hours\n",
    "        self._offset_minutes = offset_minutes\n",
    "        self._offset = offset\n",
    "        \n",
    "    @property\n",
    "    def offset(self):\n",
    "        return self._offset\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name\n",
    "    \n",
    "    def __eq__(self, other):\n",
    "        return (isinstance(other, TimeZone) and \n",
    "                self.name == other.name and \n",
    "                self._offset_hours == other._offset_hours and\n",
    "                self._offset_minutes == other._offset_minutes)\n",
    "    def __repr__(self):\n",
    "        return (f\"TimeZone(name='{self.name}', \"\n",
    "                f\"offset_hours={self._offset_hours}, \"\n",
    "                f\"offset_minutes={self._offset_minutes})\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As I mentioned earlier the code should contain:\n",
    "- the transaction code\n",
    "- the account number\n",
    "- the date / time in UTC of the transaction\n",
    "- the transaction number\n",
    "\n",
    "Something like:\n",
    "```D-140568-20190115154500-124```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's first build this function in isolation, and then we'll add it to the class once we're happy with it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_confirmation_code(account_number, transaction_id, transaction_code):\n",
    "    # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "    # YYYYMMDDHHMMSS\n",
    "    dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "    return f'{transaction_code}-{account_number}-{dt_str}-{transaction_id}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'X-123-20190602232855-1000'"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "generate_confirmation_code(123, 1000, 'X')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's incorporate that as an instance method in our class - we won't need to pass account_numberor transaction_id - we can get those ourselves from within the class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we can test it out, let's write a dummy transaction method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'\n",
    "    \n",
    "    def make_transaction(self):\n",
    "        return self.generate_confirmation_code('dummy')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = Account('A100', 'John', 'Cleese', initial_balance=100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'dummy-A100-20190602232855-100'"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.make_transaction()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'dummy-A100-20190602232855-101'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a.make_transaction()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Confirmation Parser"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This one is actually going to be a static method. We don't really require the instance to reverse-engineer the confirmation code. We could, but then we probably would also want to check that the account number embedded in the conformation code is the same as the instance account number we are decoding from. There's not really a need for that.\n",
    "\n",
    "The other thing is we want to have a neat dotted notation to recover the various properties of the partsed confirmation code.\n",
    "Once again, before jumping into creating a utility class for that, we can really get away with a named tuple here instead!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see what a confirmation code looks like:\n",
    "```\n",
    "dummy-A100-20190325224918-101\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, we can split things on a `-` symbol, and we'll have to parse the date time string into an actual date time (and we know confirmation codes use UTC dates)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple\n",
    "\n",
    "Confirmation = namedtuple('Confirmation', 'account_number, transaction_code, transaction_id, time_utc, time')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    transaction_counter = itertools.count(100)\n",
    "    _interest_rate = 0.5  # percentage\n",
    "    \n",
    "    _transaction_codes = {\n",
    "        'deposit': 'D',\n",
    "        'withdraw': 'W',\n",
    "        'interest': 'I',\n",
    "        'rejected': 'X'\n",
    "    }\n",
    "    \n",
    "    def __init__(self, account_number, first_name, last_name, timezone=None, initial_balance=0):\n",
    "        # in practice we probably would want to add checks to make sure these values are valid / non-empty\n",
    "        self._account_number = account_number\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        \n",
    "        if timezone is None:\n",
    "            timezone = TimeZone('UTC', 0, 0)\n",
    "        self.timezone = timezone\n",
    "        \n",
    "        self._balance = float(initial_balance)  # force use of floats here, but maybe Decimal would be better\n",
    "        \n",
    "    @property\n",
    "    def account_number(self):\n",
    "        return self._account_number\n",
    "    \n",
    "    @property \n",
    "    def first_name(self):\n",
    "        return self._first_name\n",
    "    \n",
    "    @first_name.setter\n",
    "    def first_name(self, value):\n",
    "        self.validate_and_set_name('_first_name', value, 'First Name')\n",
    "        \n",
    "    @property\n",
    "    def last_name(self):\n",
    "        return self._last_name\n",
    "    \n",
    "    @last_name.setter\n",
    "    def last_name(self, value):\n",
    "        self.validate_and_set_name('_last_name', value, 'Last Name')\n",
    "        \n",
    "    # also going to create a full_name computed property, for ease of use\n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return f'{self.first_name} {self.last_name}'\n",
    "        \n",
    "    @property\n",
    "    def timezone(self):\n",
    "        return self._timezone\n",
    "    \n",
    "    @property\n",
    "    def balance(self):\n",
    "        return self._balance\n",
    "    \n",
    "    @timezone.setter\n",
    "    def timezone(self, value):\n",
    "        if not isinstance(value, TimeZone):\n",
    "            raise ValueError('Time zone must be a valid TimeZone object.')\n",
    "        self._timezone = value\n",
    "          \n",
    "    @classmethod\n",
    "    def get_interest_rate(cls):\n",
    "        return cls._interest_rate\n",
    "    \n",
    "    @classmethod\n",
    "    def set_interest_rate(cls, value):\n",
    "        if not isinstance(value, numbers.Real):\n",
    "            raise ValueError('Interest rate must be a real number')\n",
    "        if value < 0:\n",
    "            raise ValueError('Interest rate cannot be negative.')\n",
    "        cls._interest_rate = value\n",
    "        \n",
    "    def validate_and_set_name(self, property_name, value, field_title):\n",
    "        if value is None or len(str(value).strip()) == 0:\n",
    "            raise ValueError(f'{field_title} cannot be empty.')\n",
    "        setattr(self, property_name, value)\n",
    "        \n",
    "    def generate_confirmation_code(self, transaction_code):\n",
    "        # main difficulty here is to generate the current time in UTC using this formatting:\n",
    "        # YYYYMMDDHHMMSS\n",
    "        dt_str = datetime.utcnow().strftime('%Y%m%d%H%M%S')\n",
    "        return f'{transaction_code}-{self.account_number}-{dt_str}-{next(Account.transaction_counter)}'\n",
    "    \n",
    "    @staticmethod\n",
    "    def parse_confirmation_code(confirmation_code, preferred_time_zone=None):\n",
    "        # dummy-A100-20190325224918-101\n",
    "        parts = confirmation_code.split('-')\n",
    "        if len(parts) != 4:\n",
    "            # really simplistic validation here - would need something better\n",
    "            raise ValueError('Invalid confirmation code')\n",
    "        \n",
    "        # unpack into separate variables\n",
    "        transaction_code, account_number, raw_dt_utc, transaction_id = parts\n",
    "        \n",
    "        # need to convert raw_dt_utc into a proper datetime object\n",
    "        try:\n",
    "            dt_utc = datetime.strptime(raw_dt_utc, '%Y%m%d%H%M%S')\n",
    "        except ValueError as ex:\n",
    "            # again, probably need better error handling here\n",
    "            raise ValueError('Invalid transaction datetime') from ex\n",
    "          \n",
    "        if preferred_time_zone is None:\n",
    "            preferred_time_zone = TimeZone('UTC', 0, 0)\n",
    "            \n",
    "        if not isinstance(preferred_time_zone, TimeZone):\n",
    "            raise ValueError('Invalid TimeZone specified.')\n",
    "            \n",
    "        dt_preferred = dt_utc + preferred_time_zone.offset\n",
    "        dt_preferred_str = f\"{dt_preferred.strftime('%Y-%m-%d %H:%M:%S')} ({preferred_time_zone.name})\"\n",
    "        \n",
    "        return Confirmation(account_number, transaction_code, transaction_id, dt_utc.isoformat(), dt_preferred_str)\n",
    "    \n",
    "    def make_transaction(self):\n",
    "        return self.generate_confirmation_code('dummy')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so let's try this out, and make sure it's working (within reason, this will not catch all badly formatted confirmation codes by any means):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dummy-A100-20190602232855-100\n"
     ]
    }
   ],
   "source": [
    "a = Account('A100', 'John', 'Cleese', initial_balance=100)\n",
    "conf_code = a.make_transaction()\n",
    "print(conf_code)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Confirmation(account_number='A100', transaction_code='dummy', transaction_id='100', time_utc='2019-06-02T23:28:55', time='2019-06-02 23:28:55 (UTC)')"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Account.parse_confirmation_code(conf_code)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Confirmation(account_number='A100', transaction_code='dummy', transaction_id='100', time_utc='2019-06-02T23:28:55', time='2019-06-02 16:28:55 (MST)')"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Account.parse_confirmation_code(conf_code, TimeZone('MST', -7, 0))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Invalid transaction datetime\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    Account.parse_confirmation_code('X-A100-asdasd-123')\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
